Passed
Branch v8.x (ebc870)
by Rafael S.
01:57
created

index.js ➔ LEorBE_   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 7
rs 10
1
/*
2
 * Copyright (c) 2017-2018 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFile class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import bitDepthLib from './vendor/bitdepth.js';
33
import * as imaadpcm from './vendor/imaadpcm.js';
34
import * as alawmulaw from './vendor/alawmulaw.js';
35
import {encode, decode} from './vendor/base64-arraybuffer-es6.js';
36
import {unpackArray, packArrayTo, unpackArrayTo} from './vendor/byte-data.js';
37
import {wavHeader, validateHeader_} from './lib/wavheader.js';
38
import {riffChunks, findChunk_} from './vendor/riff-chunks.js';
39
import BufferIO from './lib/bufferio.js';
40
import writeWavBuffer from './lib/wav-buffer-writer.js';
41
import readWavBuffer from './lib/wav-buffer-reader.js';
42
43
/**
44
 * Class representing a wav file.
45
 * @ignore
46
 */
47
export default class WaveFile {
48
49
  /**
50
   * @param {?Uint8Array} bytes A wave file buffer.
51
   * @throws {Error} If no 'RIFF' chunk is found.
52
   * @throws {Error} If no 'fmt ' chunk is found.
53
   * @throws {Error} If no 'data' chunk is found.
54
   */
55
  constructor(bytes=null) {
56
    /**
57
     * The container identifier.
58
     * 'RIFF', 'RIFX' and 'RF64' are supported.
59
     * @type {string}
60
     */
61
    this.container = '';
62
    /**
63
     * @type {number}
64
     */
65
    this.chunkSize = 0;
66
    /**
67
     * The format.
68
     * Always 'WAVE'.
69
     * @type {string}
70
     */
71
    this.format = '';
72
    /**
73
     * The data of the 'fmt' chunk.
74
     * @type {!Object<string, *>}
75
     */
76
    this.fmt = {
77
      /** @type {string} */
78
      chunkId: '',
79
      /** @type {number} */
80
      chunkSize: 0,
81
      /** @type {number} */
82
      audioFormat: 0,
83
      /** @type {number} */
84
      numChannels: 0,
85
      /** @type {number} */
86
      sampleRate: 0,
87
      /** @type {number} */
88
      byteRate: 0,
89
      /** @type {number} */
90
      blockAlign: 0,
91
      /** @type {number} */
92
      bitsPerSample: 0,
93
      /** @type {number} */
94
      cbSize: 0,
95
      /** @type {number} */
96
      validBitsPerSample: 0,
97
      /** @type {number} */
98
      dwChannelMask: 0,
99
      /**
100
       * 4 32-bit values representing a 128-bit ID
101
       * @type {!Array<number>}
102
       */
103
      subformat: []
104
    };
105
    /**
106
     * The data of the 'fact' chunk.
107
     * @type {!Object<string, *>}
108
     */
109
    this.fact = {
110
      /** @type {string} */
111
      chunkId: '',
112
      /** @type {number} */
113
      chunkSize: 0,
114
      /** @type {number} */
115
      dwSampleLength: 0
116
    };
117
    /**
118
     * The data of the 'cue ' chunk.
119
     * @type {!Object<string, *>}
120
     */
121
    this.cue = {
122
      /** @type {string} */
123
      chunkId: '',
124
      /** @type {number} */
125
      chunkSize: 0,
126
      /** @type {number} */
127
      dwCuePoints: 0,
128
      /** @type {!Array<!Object>} */
129
      points: [],
130
    };
131
    /**
132
     * The data of the 'smpl' chunk.
133
     * @type {!Object<string, *>}
134
     */
135
    this.smpl = {
136
      /** @type {string} */
137
      chunkId: '',
138
      /** @type {number} */
139
      chunkSize: 0,
140
      /** @type {number} */
141
      dwManufacturer: 0,
142
      /** @type {number} */
143
      dwProduct: 0,
144
      /** @type {number} */
145
      dwSamplePeriod: 0,
146
      /** @type {number} */
147
      dwMIDIUnityNote: 0,
148
      /** @type {number} */
149
      dwMIDIPitchFraction: 0,
150
      /** @type {number} */
151
      dwSMPTEFormat: 0,
152
      /** @type {number} */
153
      dwSMPTEOffset: 0,
154
      /** @type {number} */
155
      dwNumSampleLoops: 0,
156
      /** @type {number} */
157
      dwSamplerData: 0,
158
      /** @type {!Array<!Object>} */
159
      loops: []
160
    };
161
    /**
162
     * The data of the 'bext' chunk.
163
     * @type {!Object<string, *>}
164
     */
165
    this.bext = {
166
      /** @type {string} */
167
      chunkId: '',
168
      /** @type {number} */
169
      chunkSize: 0,
170
      /** @type {string} */
171
      description: '', //256
172
      /** @type {string} */
173
      originator: '', //32
174
      /** @type {string} */
175
      originatorReference: '', //32
176
      /** @type {string} */
177
      originationDate: '', //10
178
      /** @type {string} */
179
      originationTime: '', //8
180
      /**
181
       * 2 32-bit values, timeReference high and low
182
       * @type {!Array<number>}
183
       */
184
      timeReference: [0, 0],
185
      /** @type {number} */
186
      version: 0, //WORD
187
      /** @type {string} */
188
      UMID: '', // 64 chars
189
      /** @type {number} */
190
      loudnessValue: 0, //WORD
191
      /** @type {number} */
192
      loudnessRange: 0, //WORD
193
      /** @type {number} */
194
      maxTruePeakLevel: 0, //WORD
195
      /** @type {number} */
196
      maxMomentaryLoudness: 0, //WORD
197
      /** @type {number} */
198
      maxShortTermLoudness: 0, //WORD
199
      /** @type {string} */
200
      reserved: '', //180
201
      /** @type {string} */
202
      codingHistory: '' // string, unlimited
203
    };
204
    /**
205
     * The data of the 'ds64' chunk.
206
     * Used only with RF64 files.
207
     * @type {!Object<string, *>}
208
     */
209
    this.ds64 = {
210
      /** @type {string} */
211
      chunkId: '',
212
      /** @type {number} */
213
      chunkSize: 0,
214
      /** @type {number} */
215
      riffSizeHigh: 0, // DWORD
216
      /** @type {number} */
217
      riffSizeLow: 0, // DWORD
218
      /** @type {number} */
219
      dataSizeHigh: 0, // DWORD
220
      /** @type {number} */
221
      dataSizeLow: 0, // DWORD
222
      /** @type {number} */
223
      originationTime: 0, // DWORD
224
      /** @type {number} */
225
      sampleCountHigh: 0, // DWORD
226
      /** @type {number} */
227
      sampleCountLow: 0 // DWORD
228
      /** @type {number} */
229
      //'tableLength': 0, // DWORD
230
      /** @type {!Array<number>} */
231
      //'table': []
232
    };
233
    /**
234
     * The data of the 'data' chunk.
235
     * @type {!Object<string, *>}
236
     */
237
    this.data = {
238
      /** @type {string} */
239
      chunkId: '',
240
      /** @type {number} */
241
      chunkSize: 0,
242
      /** @type {!Uint8Array} */
243
      samples: new Uint8Array(0)
244
    };
245
    /**
246
     * The data of the 'LIST' chunks.
247
     * Each item in this list look like this:
248
     *  {
249
     *      chunkId: '',
250
     *      chunkSize: 0,
251
     *      format: '',
252
     *      subChunks: []
253
     *   }
254
     * @type {!Array<!Object>}
255
     */
256
    this.LIST = [];
257
    /**
258
     * The data of the 'junk' chunk.
259
     * @type {!Object<string, *>}
260
     */
261
    this.junk = {
262
      /** @type {string} */
263
      chunkId: '',
264
      /** @type {number} */
265
      chunkSize: 0,
266
      /** @type {!Array<number>} */
267
      chunkData: []
268
    };
269
    /**
270
     * The bit depth code according to the samples.
271
     * @type {string}
272
     */
273
    this.bitDepth = '0';
274
    /**
275
     * @type {!Object}
276
     * @private
277
     */
278
    this.dataType = {};
279
    this.io = new BufferIO();
280
    // Load a file from the buffer if one was passed
281
    // when creating the object
282
    if(bytes) {
283
      this.fromBuffer(bytes);
284
    }
285
  }
286
287
  /**
288
   * Set up the WaveFile object based on the arguments passed.
289
   * @param {number} numChannels The number of channels
290
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
291
   * @param {number} sampleRate The sample rate.
292
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
293
   * @param {string} bitDepthCode The audio bit depth code.
294
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
295
   *    or any value between '8' and '32' (like '12').
296
   * @param {!Array<number>|!Array<!Array<number>>|!ArrayBufferView} samples
297
   *    The samples. Must be in the correct range according to the bit depth.
298
   * @param {?Object} options Optional. Used to force the container
299
   *    as RIFX with {'container': 'RIFX'}
300
   * @throws {Error} If any argument does not meet the criteria.
301
   */
302
  fromScratch(numChannels, sampleRate, bitDepthCode, samples, options={}) {
303
    if (!options.container) {
304
      options.container = 'RIFF';
305
    }
306
    this.container = options.container;
307
    this.bitDepth = bitDepthCode;
308
    samples = this.interleave_(samples);
309
    this.updateDataType_();
310
    /** @type {number} */
311
    let numBytes = this.dataType.bits / 8;
312
    this.data.samples = new Uint8Array(samples.length * numBytes);
313
    packArrayTo(samples, this.dataType, this.data.samples);
314
    /** @type {!Object} */
315
    let header = wavHeader(
316
      bitDepthCode, numChannels, sampleRate,
317
      numBytes, this.data.samples.length, options);
318
    this.clearHeader_();
319
    this.chunkSize = header.chunkSize;
320
    this.format = header.format;
321
    this.fmt = header.fmt;
322
    if (header.fact) {
323
      this.fact = header.fact;
324
    }
325
    this.data.chunkId = 'data';
326
    this.data.chunkSize = this.data.samples.length;
327
    validateHeader_(this);
328
  }
329
330
  /**
331
   * Set up the WaveFile object from a byte buffer.
332
   * @param {!Uint8Array} bytes The buffer.
333
   * @param {boolean=} samples True if the samples should be loaded.
334
   * @throws {Error} If container is not RIFF, RIFX or RF64.
335
   * @throws {Error} If no 'fmt ' chunk is found.
336
   * @throws {Error} If no 'data' chunk is found.
337
   */
338
  fromBuffer(bytes, samples=true) {
339
    this.clearHeader_();
340
    readWavBuffer(bytes, samples, this);
341
    this.updateDataType_();
342
  }
343
344
  /**
345
   * Return a byte buffer representig the WaveFile object as a .wav file.
346
   * The return value of this method can be written straight to disk.
347
   * @return {!Uint8Array} A .wav file.
348
   * @throws {Error} If any property of the object appears invalid.
349
   */
350
  toBuffer() {
351
    validateHeader_(this);
352
    return writeWavBuffer(this);
353
  }
354
355
  /**
356
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
357
   * @param {string} base64String A .wav file as a base64 string.
358
   * @throws {Error} If any property of the object appears invalid.
359
   */
360
  fromBase64(base64String) {
361
    this.fromBuffer(new Uint8Array(decode(base64String)));
362
  }
363
364
  /**
365
   * Return a base64 string representig the WaveFile object as a .wav file.
366
   * @return {string} A .wav file as a base64 string.
367
   * @throws {Error} If any property of the object appears invalid.
368
   */
369
  toBase64() {
370
    /** @type {!Uint8Array} */
371
    let buffer = this.toBuffer();
372
    return encode(buffer, 0, buffer.length);
373
  }
374
375
  /**
376
   * Return a DataURI string representig the WaveFile object as a .wav file.
377
   * The return of this method can be used to load the audio in browsers.
378
   * @return {string} A .wav file as a DataURI.
379
   * @throws {Error} If any property of the object appears invalid.
380
   */
381
  toDataURI() {
382
    return 'data:audio/wav;base64,' + this.toBase64();
383
  }
384
385
  /**
386
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
387
   * @param {string} dataURI A .wav file as DataURI.
388
   * @throws {Error} If any property of the object appears invalid.
389
   */
390
  fromDataURI(dataURI) {
391
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
392
  }
393
394
  /**
395
   * Force a file as RIFF.
396
   */
397
  toRIFF() {
398
    if (this.container == 'RF64') {
399
      this.fromScratch(
400
        this.fmt.numChannels,
401
        this.fmt.sampleRate,
402
        this.bitDepth,
403
        unpackArray(this.data.samples, this.dataType));
404
    } else {
405
      this.dataType.be = true;
406
      this.fromScratch(
407
        this.fmt.numChannels,
408
        this.fmt.sampleRate,
409
        this.bitDepth,
410
        unpackArray(this.data.samples, this.dataType));
411
    }
412
  }
413
414
  /**
415
   * Force a file as RIFX.
416
   */
417
  toRIFX() {
418
    if (this.container == 'RF64') {
419
      this.fromScratch(
420
        this.fmt.numChannels,
421
        this.fmt.sampleRate,
422
        this.bitDepth,
423
        unpackArray(this.data.samples, this.dataType),
424
        {container: 'RIFX'});
425
    } else {
426
      this.fromScratch(
427
        this.fmt.numChannels,
428
        this.fmt.sampleRate,
429
        this.bitDepth,
430
        unpackArray(this.data.samples, this.dataType),
431
        {container: 'RIFX'});
432
    }
433
  }
434
435
  /**
436
   * Change the bit depth of the samples.
437
   * @param {string} newBitDepth The new bit depth of the samples.
438
   *    One of '8' ... '32' (integers), '32f' or '64' (floats)
439
   * @param {boolean} changeResolution A boolean indicating if the
440
   *    resolution of samples should be actually changed or not.
441
   * @throws {Error} If the bit depth is not valid.
442
   */
443
  toBitDepth(newBitDepth, changeResolution=true) {
444
    /** @type {string} */
445
    let toBitDepth = newBitDepth;
446
    /** @type {string} */
447
    let thisBitDepth = this.bitDepth;
448
    if (!changeResolution) {
449
      if (newBitDepth != '32f') {
450
        toBitDepth = this.dataType.bits.toString();
451
      }
452
      thisBitDepth = this.dataType.bits;
453
    }
454
    this.assureUncompressed_();
455
    /** @type {number} */
456
    let sampleCount = this.data.samples.length / (this.dataType.bits / 8);
457
    /** @type {!Float64Array} */
458
    let typedSamplesInput = new Float64Array(sampleCount + 1);
459
    /** @type {!Float64Array} */
460
    let typedSamplesOutput = new Float64Array(sampleCount + 1);
461
    unpackArrayTo(this.data.samples, this.dataType, typedSamplesInput);
462
    bitDepthLib(
463
      typedSamplesInput, thisBitDepth, toBitDepth, typedSamplesOutput);
464
    this.fromScratch(
465
      this.fmt.numChannels,
466
      this.fmt.sampleRate,
467
      newBitDepth,
468
      typedSamplesOutput,
469
      {container: this.correctContainer_()});
470
  }
471
472
  /**
473
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
474
   * @throws {Error} If sample rate is not 8000.
475
   * @throws {Error} If number of channels is not 1.
476
   */
477
  toIMAADPCM() {
478
    if (this.fmt.sampleRate !== 8000) {
479
      throw new Error(
480
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
481
    } else if(this.fmt.numChannels !== 1) {
482
      throw new Error(
483
        'Only mono files can be compressed as IMA-ADPCM.');
484
    } else {
485
      this.assure16Bit_();
486
      let output = new Int16Array(this.data.samples.length / 2);
487
      unpackArrayTo(this.data.samples, this.dataType, output);
488
      this.fromScratch(
489
        this.fmt.numChannels,
490
        this.fmt.sampleRate,
491
        '4',
492
        imaadpcm.encode(output),
493
        {container: this.correctContainer_()});
494
    }
495
  }
496
497
  /**
498
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
499
   * @param {string} bitDepthCode The new bit depth of the samples.
500
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
501
   *    Optional. Default is 16.
502
   */
503
  fromIMAADPCM(bitDepthCode='16') {
504
    this.fromScratch(
505
      this.fmt.numChannels,
506
      this.fmt.sampleRate,
507
      '16',
508
      imaadpcm.decode(this.data.samples, this.fmt.blockAlign),
509
      {container: this.correctContainer_()});
510
    if (bitDepthCode != '16') {
511
      this.toBitDepth(bitDepthCode);
512
    }
513
  }
514
515
  /**
516
   * Encode a 16-bit wave file as 8-bit A-Law.
517
   */
518
  toALaw() {
519
    this.assure16Bit_();
520
    let output = new Int16Array(this.data.samples.length / 2);
521
    unpackArrayTo(this.data.samples, this.dataType, output);
522
    this.fromScratch(
523
      this.fmt.numChannels,
524
      this.fmt.sampleRate,
525
      '8a',
526
      alawmulaw.alaw.encode(output),
527
      {container: this.correctContainer_()});
528
  }
529
530
  /**
531
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
532
   * @param {string} bitDepthCode The new bit depth of the samples.
533
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
534
   *    Optional. Default is 16.
535
   */
536
  fromALaw(bitDepthCode='16') {
537
    this.fromScratch(
538
      this.fmt.numChannels,
539
      this.fmt.sampleRate,
540
      '16',
541
      alawmulaw.alaw.decode(this.data.samples),
542
      {container: this.correctContainer_()});
543
    if (bitDepthCode != '16') {
544
      this.toBitDepth(bitDepthCode);
545
    }
546
  }
547
548
  /**
549
   * Encode 16-bit wave file as 8-bit mu-Law.
550
   */
551
  toMuLaw() {
552
    this.assure16Bit_();
553
    let output = new Int16Array(this.data.samples.length / 2);
554
    unpackArrayTo(this.data.samples, this.dataType, output);
555
    this.fromScratch(
556
      this.fmt.numChannels,
557
      this.fmt.sampleRate,
558
      '8m',
559
      alawmulaw.mulaw.encode(output),
560
      {container: this.correctContainer_()});
561
  }
562
563
  /**
564
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
565
   * @param {string} bitDepthCode The new bit depth of the samples.
566
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
567
   *    Optional. Default is 16.
568
   */
569
  fromMuLaw(bitDepthCode='16') {
570
    this.fromScratch(
571
      this.fmt.numChannels,
572
      this.fmt.sampleRate,
573
      '16',
574
      alawmulaw.mulaw.decode(this.data.samples),
575
      {container: this.correctContainer_()});
576
    if (bitDepthCode != '16') {
577
      this.toBitDepth(bitDepthCode);
578
    }
579
  }
580
581
  /**
582
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
583
   * then it is created. It if exists, it is overwritten.
584
   * @param {string} tag The tag name.
585
   * @param {string} value The tag value.
586
   * @throws {Error} If the tag name is not valid.
587
   */
588
  setTag(tag, value) {
589
    tag = this.fixTagName_(tag);
590
    /** @type {!Object} */
591
    let index = this.getTagIndex_(tag);
592
    if (index.TAG !== null) {
593
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
594
        value.length + 1;
595
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
596
    } else if (index.LIST !== null) {
597
      this.LIST[index.LIST].subChunks.push({
598
        chunkId: tag,
599
        chunkSize: value.length + 1,
600
        value: value});
601
    } else {
602
      this.LIST.push({
603
        chunkId: 'LIST',
604
        chunkSize: 8 + value.length + 1,
605
        format: 'INFO',
606
        subChunks: []});
607
      this.LIST[this.LIST.length - 1].subChunks.push({
608
        chunkId: tag,
609
        chunkSize: value.length + 1,
610
        value: value});
611
    }
612
  }
613
614
  /**
615
   * Return the value of a RIFF tag in the INFO chunk.
616
   * @param {string} tag The tag name.
617
   * @return {?string} The value if the tag is found, null otherwise.
618
   */
619
  getTag(tag) {
620
    /** @type {!Object} */
621
    let index = this.getTagIndex_(tag);
622
    if (index.TAG !== null) {
623
      return this.LIST[index.LIST].subChunks[index.TAG].value;
624
    }
625
    return null;
626
  }
627
628
  /**
629
   * Remove a RIFF tag in the INFO chunk.
630
   * @param {string} tag The tag name.
631
   * @return {boolean} True if a tag was deleted.
632
   */
633
  deleteTag(tag) {
634
    /** @type {!Object} */
635
    let index = this.getTagIndex_(tag);
636
    if (index.TAG !== null) {
637
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
638
      return true;
639
    }
640
    return false;
641
  }
642
643
  /**
644
   * Create a cue point in the wave file.
645
   * @param {number} position The cue point position in milliseconds.
646
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
647
   */
648
  setCuePoint(position, labl='') {
649
    this.cue.chunkId = 'cue ';
650
    position = (position * this.fmt.sampleRate) / 1000;
651
    /** @type {!Array<!Object>} */
652
    let existingPoints = this.getCuePoints_();
653
    this.clearLISTadtl_();
654
    /** @type {number} */
655
    let len = this.cue.points.length;
656
    this.cue.points = [];
657
    /** @type {boolean} */
658
    let hasSet = false;
659
    if (len === 0) {
660
      this.setCuePoint_(position, 1, labl);
661
    } else {
662
      for (let i=0; i<len; i++) {
663
        if (existingPoints[i].dwPosition > position && !hasSet) {
664
          this.setCuePoint_(position, i + 1, labl);
665
          this.setCuePoint_(
666
            existingPoints[i].dwPosition,
667
            i + 2,
668
            existingPoints[i].label);
669
          hasSet = true;
670
        } else {
671
          this.setCuePoint_(
672
            existingPoints[i].dwPosition,
673
            i + 1,
674
            existingPoints[i].label);
675
        }
676
      }
677
      if (!hasSet) {
678
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
679
      }
680
    }
681
    this.cue.dwCuePoints = this.cue.points.length;
682
  }
683
684
  /**
685
   * Remove a cue point from a wave file.
686
   * @param {number} index the index of the point. First is 1,
687
   *    second is 2, and so on.
688
   */
689
  deleteCuePoint(index) {
690
    this.cue.chunkId = 'cue ';
691
    /** @type {!Array<!Object>} */
692
    let existingPoints = this.getCuePoints_();
693
    this.clearLISTadtl_();
694
    /** @type {number} */
695
    let len = this.cue.points.length;
696
    this.cue.points = [];
697
    for (let i=0; i<len; i++) {
698
      if (i + 1 !== index) {
699
        this.setCuePoint_(
700
          existingPoints[i].dwPosition,
701
          i + 1,
702
          existingPoints[i].label);
703
      }
704
    }
705
    this.cue.dwCuePoints = this.cue.points.length;
706
    if (this.cue.dwCuePoints) {
707
      this.cue.chunkId = 'cue ';
708
    } else {
709
      this.cue.chunkId = '';
710
      this.clearLISTadtl_();
711
    }
712
  }
713
714
  /**
715
   * Update the label of a cue point.
716
   * @param {number} pointIndex The ID of the cue point.
717
   * @param {string} label The new text for the label.
718
   */
719
  updateLabel(pointIndex, label) {
720
    /** @type {?number} */
721
    let adtlIndex = this.getAdtlChunk_();
722
    if (adtlIndex !== null) {
723
      for (let i=0; i<this.LIST[adtlIndex].subChunks.length; i++) {
724
        if (this.LIST[adtlIndex].subChunks[i].dwName ==
725
            pointIndex) {
726
          this.LIST[adtlIndex].subChunks[i].value = label;
727
        }
728
      }
729
    }
730
  }
731
732
  /**
733
   * Push a new cue point in this.cue.points.
734
   * @param {number} position The position in milliseconds.
735
   * @param {number} dwName the dwName of the cue point
736
   * @private
737
   */
738
  setCuePoint_(position, dwName, label) {
739
    this.cue.points.push({
740
      dwName: dwName,
741
      dwPosition: position,
742
      fccChunk: 'data',
743
      dwChunkStart: 0,
744
      dwBlockStart: 0,
745
      dwSampleOffset: position,
746
    });
747
    this.setLabl_(dwName, label);
748
  }
749
750
  /**
751
   * Return an array with the position of all cue points in the file.
752
   * @return {!Array<!Object>}
753
   * @private
754
   */
755
  getCuePoints_() {
756
    /** @type {!Array<!Object>} */
757
    let points = [];
758
    for (let i=0; i<this.cue.points.length; i++) {
759
      points.push({
760
        dwPosition: this.cue.points[i].dwPosition,
761
        label: this.getLabelForCuePoint_(
762
          this.cue.points[i].dwName)});
763
    }
764
    return points;
765
  }
766
767
  /**
768
   * Return the label of a cue point.
769
   * @param {number} pointDwName The ID of the cue point.
770
   * @return {string}
771
   * @private
772
   */
773
  getLabelForCuePoint_(pointDwName) {
774
    /** @type {?number} */
775
    let adtlIndex = this.getAdtlChunk_();
776
    if (adtlIndex !== null) {
777
      for (let i=0; i<this.LIST[adtlIndex].subChunks.length; i++) {
778
        if (this.LIST[adtlIndex].subChunks[i].dwName ==
779
            pointDwName) {
780
          return this.LIST[adtlIndex].subChunks[i].value;
781
        }
782
      }
783
    }
784
    return '';
785
  }
786
787
  /**
788
   * Clear any LIST chunk labeled as 'adtl'.
789
   * @private
790
   */
791
  clearLISTadtl_() {
792
    for (let i=0; i<this.LIST.length; i++) {
793
      if (this.LIST[i].format == 'adtl') {
794
        this.LIST.splice(i);
795
      }
796
    }
797
  }
798
799
  /**
800
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
801
   * @param {number} dwName The ID of the cue point.
802
   * @param {string} label The label for the cue point.
803
   * @private
804
   */
805
  setLabl_(dwName, label) {
806
    /** @type {?number} */
807
    let adtlIndex = this.getAdtlChunk_();
808
    if (adtlIndex === null) {
809
      this.LIST.push({
810
        chunkId: 'LIST',
811
        chunkSize: 4,
812
        format: 'adtl',
813
        subChunks: []});
814
      adtlIndex = this.LIST.length - 1;
815
    }
816
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
817
  }
818
819
  /**
820
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
821
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
822
   * @param {number} dwName The ID of the cue point.
823
   * @param {string} label The label for the cue point.
824
   * @private
825
   */
826
  setLabelText_(adtlIndex, dwName, label) {
827
    this.LIST[adtlIndex].subChunks.push({
828
      chunkId: 'labl',
829
      chunkSize: label.length,
830
      dwName: dwName,
831
      value: label
832
    });
833
    this.LIST[adtlIndex].chunkSize += label.length + 4 + 4 + 4 + 1;
834
  }
835
836
  /**
837
   * Return the index of the 'adtl' LIST in this.LIST.
838
   * @return {?number}
839
   * @private
840
   */
841
  getAdtlChunk_() {
842
    for (let i=0; i<this.LIST.length; i++) {
843
      if(this.LIST[i].format == 'adtl') {
844
        return i;
845
      }
846
    }
847
    return null;
848
  }
849
850
  /**
851
   * Return the index of a tag in a FILE chunk.
852
   * @param {string} tag The tag name.
853
   * @return {!Object<string, ?number>}
854
   *    Object.LIST is the INFO index in LIST
855
   *    Object.TAG is the tag index in the INFO
856
   * @private
857
   */
858
  getTagIndex_(tag) {
859
    /** @type {!Object<string, ?number>} */
860
    let index = {LIST: null, TAG: null};
861
    for (let i=0; i<this.LIST.length; i++) {
862
      if (this.LIST[i].format == 'INFO') {
863
        index.LIST = i;
864
        for (let j=0; j<this.LIST[i].subChunks.length; j++) {
865
          if (this.LIST[i].subChunks[j].chunkId == tag) {
866
            index.TAG = j;
867
            break;
868
          }
869
        }
870
        break;
871
      }
872
    }
873
    return index;
874
  }
875
876
  /**
877
   * Fix a RIFF tag format if possible, throw an error otherwise.
878
   * @param {string} tag The tag name.
879
   * @return {string} The tag name in proper fourCC format.
880
   * @private
881
   */
882
  fixTagName_(tag) {
883
    if (tag.constructor !== String) {
884
      throw new Error('Invalid tag name.');
885
    } else if(tag.length < 4) {
886
      for (let i=0; i<4-tag.length; i++) {
887
        tag += ' ';
888
      }
889
    }
890
    return tag;
891
  }
892
893
  /**
894
   * Reset attributes that should emptied when a file is
895
   * created with the fromScratch() or fromBuffer() methods.
896
   * @private
897
   */
898
  clearHeader_() {
899
    this.fmt.cbSize = 0;
900
    this.fmt.validBitsPerSample = 0;
901
    this.fact.chunkId = '';
902
    this.ds64.chunkId = '';
903
  }
904
905
  /**
906
   * Make the file 16-bit if it is not.
907
   * @private
908
   */
909
  assure16Bit_() {
910
    this.assureUncompressed_();
911
    if (this.bitDepth != '16') {
912
      this.toBitDepth('16');
913
    }
914
  }
915
916
  /**
917
   * Uncompress the samples in case of a compressed file.
918
   * @private
919
   */
920
  assureUncompressed_() {
921
    if (this.bitDepth == '8a') {
922
      this.fromALaw();
923
    } else if(this.bitDepth == '8m') {
924
      this.fromMuLaw();
925
    } else if (this.bitDepth == '4') {
926
      this.fromIMAADPCM();
927
    }
928
  }
929
930
  /**
931
   * Set up the WaveFile object from a byte buffer.
932
   * @param {!Array<number>|!Array<!Array<number>>|!ArrayBufferView} samples The samples.
933
   * @private
934
   */
935
  interleave_(samples) {
936
    if (samples.length > 0) {
937
      if (samples[0].constructor === Array) {
938
        /** @type {!Array<number>} */
939
        let finalSamples = [];
940
        for (let i=0; i < samples[0].length; i++) {
941
          for (let j=0; j < samples.length; j++) {
942
            finalSamples.push(samples[j][i]);
943
          }
944
        }
945
        samples = finalSamples;
946
      }
947
    }
948
    return samples;
949
  }
950
951
  /**
952
   * Update the type definition used to read and write the samples.
953
   * @private
954
   */
955
  updateDataType_() {
956
    /** @type {!Object} */
957
    this.dataType = {
958
      bits: ((parseInt(this.bitDepth, 10) - 1) | 7) + 1,
959
      float: this.bitDepth == '32f' || this.bitDepth == '64',
960
      signed: this.bitDepth != '8',
961
      be: this.container == 'RIFX'
962
    };
963
    if (['4', '8a', '8m'].indexOf(this.bitDepth) > -1 ) {
964
      this.dataType.bits = 8;
965
      this.dataType.signed = false;
966
    }
967
  }
968
969
  /**
970
   * Return 'RIFF' if the container is 'RF64', the current container name
971
   * otherwise. Used to enforce 'RIFF' when RF64 is not allowed.
972
   * @return {string}
973
   * @private
974
   */
975
  correctContainer_() {
976
    return this.container == 'RF64' ? 'RIFF' : this.container;
977
  }
978
}
979